package com.mlming.simpledb.backend.common;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.mlming.simpledb.backend.dm.dataItem.DataItemImpl;
import com.mlming.simpledb.common.Error;

/**
 * 通用的  引用计数法 缓存框架
 */
public abstract class AbstractCache<T> {
    /**
     * 当资源不在缓存时的读取磁盘行为
     * 根据不同模块中的实现类的行为的不同 来具体实现
     */
    protected abstract T getForCache(long key) throws Exception;
    /**
     * 当资源被驱逐时的写回磁盘行为
     * 根据不同模块中的实现类的行为的不同 来具体实现
     */
    protected abstract void releaseForCache(T obj);

    private int count; // 已缓存的资源个数
    private int maxResource; // 缓存最大资源数
    private Lock lock; // 锁

    private Map<Long,T> cache;// 缓存 数据结构
    private Set<Long> getting; // 正在被某线程读取磁盘的资源
    private Map<Long,Integer> references; // 引用计数器, 同步记录cache里面的资源的引用计数

    public AbstractCache(int maxResource) {
        this.maxResource = maxResource;
        lock = new ReentrantLock();
        cache = new HashMap<>();
        getting = new HashSet<>();
        references = new HashMap<>();
    }

    /**
     * 读取数据:
     */
    // 如果缓存里有就直接返回, 如果缓存没有就得调用getForCache去磁盘里读到缓存中
    protected T get(long key) throws Exception {
        // 循环尝试获取资源
        while (true) {
            lock.lock(); // 因为涉及多线程对缓存的写操作(资源读入缓存中以及计数相关操作), 所以得加锁
            // 1) 首先检查一下当前资源是否在getting里,
            //    如果在,说明资源还不在缓存里, 且已经有别的线程在获取了, 那么当前线程应该忙等待
            if(getting.contains(key)) {
                lock.unlock();
                try {
                    Thread.sleep(1); // 睡一会, 避免本线程频繁抢占锁
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    continue;
                }
            }
            // 2) 如果不在getting里, 那只能说明当前没线程在磁盘读此数据, 就可以检查缓存里有没有了
            //    如果缓存有,就直接return即可
            //    因为是引用计数法, 既然有人get, 就说明有人引用该资源, 那么引用次数应该+1
            if(cache.containsKey(key)) {
                T obj = cache.get(key);
                references.put(key, references.get(key) + 1);
                lock.unlock();
                return obj;
            }
            // 3) 如果缓存里面没有, 则要去磁盘里读取到缓存里面
            // 在此之前, 应该先检查是否还可以读取, 即: count 是否 > maxResource,
            // 如果是, 则根据规则报类似于OOM的错误; 否则就可以去读取磁盘数据了
            if(maxResource > 0 && count == maxResource) {
                lock.unlock();
                throw Error.CacheFullException;
            }
            // 检查通过, 去磁盘读取缓存
            // 注意: 此时是在while里面, 因为我们要while死循环来尝试获取数据,
            //      但此处必然不能是while来不断读取磁盘, 所以此时这个while的任务已经完成了,
            //      应该break出去, 在外面慢慢执行读磁盘的操作, 在此之前, 我们可以先做一些准备工作:
            //      1) 先getting加入, 防止被重复读取
            //      2) count先+1, 防止被别的新资源抢占了坑位
            //      3) 后面是读磁盘操作, 很慢, 而且这段时间阻塞着锁没必要, 因为我们通过getting已经可以防止多线程并发读取磁盘了
            getting.add(key);
            count++;
            lock.unlock();
            break;
        }
        // 开始执行读磁盘操作:
        T obj = null;
        try {
            obj = getForCache(key); // 此时应该调用实现类实际实现好的getForCache来读取磁盘
        } catch(Exception e) {
            // 如果读磁盘出现问题, 此时我们注意要对之前break之前做的那些准备工作进行一个复原
            lock.lock();
            count --;
            getting.remove(key);
            lock.unlock();
            throw e;
        }
        // 读取磁盘完毕, 要对各个数据结构进行一个修改
        lock.lock();
        cache.put(key,obj);
        references.put(key,1);
        getting.remove(key);
        lock.unlock();
        // 返回读取结果
        return obj;
    }

    /**
     * 取消引用
     */
    // 对一个资源的引用计数-1, 如果此时==0, 说明没有地方在引用该资源, 应该从缓存中删除, 并同步写回到磁盘上
    protected void release(long key) {
        lock.lock(); // 要对共享资源进行修改, 所以要加锁
        try {
            int ref = references.get(key) - 1; // 引用计数 - 1
            if(ref == 0) { // 如果是0, 那就要写回磁盘文件中去
                T obj = cache.get(key); // 读取出数据
                releaseForCache(obj); // 调用实现类实现的releaseForCache来实现写盘
                references.remove(key);// 从引用计数器中删除该资源
                cache.remove(key); // 从缓存结构中删除该资源
                count --; // 资源数-1
            } else { // 如果不是0
                references.put(key, ref); // 重写 减一后的引用计数
            }
        } finally {
            lock.unlock();
        }
    }

    /**
     * 关闭缓存，把所有资源写回盘中去
     */
    protected void close() {
        lock.lock(); // 要对共享资源进行修改, 所以要加锁
        try {
            Set<Long> keys = cache.keySet();
            for (long key : keys) {
                references.put(key,1); // 将该资源的引用置为1
                release(key); // 调用release方法, 此时内部判断必然会是0, 所以会写盘
                references.remove(key); // 删除资源
                cache.remove(key); // 删除资源
            }
        } finally {
            lock.unlock();
        }
    }

}
